package dungeon;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;

import temperature.Temperature;
import be.kuleuven.cs.som.annotate.Model;
import dungeonElements.Border;
import dungeonElements.Rock;
import dungeonElements.Square;
import dungeonElements.TeleportationSquare;
import dungeonTools.Coordinate;
import dungeonTools.Direction;

/**
 * A class providing static methods for handling rooms. This class
 * works directly on dungeons and requires nothing but a single square as its
 * input.
 * 
 * @author Tom Jacobs & Ewoud De Moor
 * @version 3.0
 *
 */
public final class RoomHandler {

	/**
	 * Variable registering the dungeon this RoomHandler is currently working on
	 */
	private static Dungeon<Square> dungeon;
	
	/**
	 * Variable registering the room this RoomHandler is currently working on
	 */
	private static HashSet<Square> room;
	
	/**
	 * The order of directions in which rooms should be detected
	 */
	private static final HashMap<Direction, Direction> directionOrder = new HashMap<Direction, Direction>();
	
	/**
	 * Sets the order of directions before the class is loaded
	 */
	static{
		directionOrder.put(Direction.CEILING, Direction.FLOOR);
		directionOrder.put(Direction.FLOOR, Direction.EAST);
		directionOrder.put(Direction.EAST, Direction.WEST);
		directionOrder.put(Direction.WEST, Direction.NORTH);
		directionOrder.put(Direction.NORTH, Direction.SOUTH);
		directionOrder.put(Direction.SOUTH, Direction.CEILING);
	}
	
	private static boolean loopDetected = false;

	/**
	 * Recalculates all internal room data to weighted averages of all
	 * temperatures and all humidity levels.
	 * 
	 * @param square
	 * 			A random square inside the room that needs to be refreshed
	 * @effect All squares in the room have the same temperature in their enclosing dungeon.
	 * 			|room = dungeon.getEnclosingRoom(square);
	 * 			|Iterator<Square> roomIterator = room.iterator();
	 * 			|	while(roomIterator.hasNext()){
	 * 			|		roomIterator.next().setTemperature(
	 * 			|		calculateNewRoomTemperature(room,calculateNewRoomHumidity(room)));
	 * @effect All squares in the room have the same humidity in their enclosing dungeon.
	 * 			|room = dungeon.getEnclosingRoom(square);
	 * 			|Iterator<Square> roomIterator = room.iterator();
	 * 			|	while(roomIterator.hasNext()){
	 * 			|		roomIterator.next().setHumidity(calculateNewRoomHumidity(room));
	 * @throws IllegalArgumentException
	 * 			The square must be initialized
	 * 			|(square == null) || (square.isTerminated())
	 * @throws IllegalArgumentException
	 * 			The square must be inside any dungeon
	 * 			|!Dungeon.isSquareInsideAnyDungeon(square)
	 */
	public static void refreshEnclosingRoomData(Square square) throws IllegalArgumentException{
		if(square instanceof Rock)
			return;
		dungeon = Dungeon.getDungeonContainingSquare(square);
		room = dungeon.getEnclosingRoom(square);
		if((room == null) || (room.size() == 1) || loopDetected)
			return;
		loopDetected = true;
		double newHumidity = calculateNewRoomHumidity(room);
		Temperature newRoomTemperature = calculateNewRoomTemperature(room, newHumidity);
		Iterator<Square> roomIterator = room.iterator();
		while(roomIterator.hasNext()){
			Square changingSquare = roomIterator.next();
			changingSquare.setHumidity(newHumidity);
			changingSquare.setTemperature(newRoomTemperature);
		}
		loopDetected = false;
	}
	/**
	 * Calculates the new weighted averaged humidity for a room
	 * 
	 * @param room2
	 * 			The room for which to calculate the new humidity value
	 * @return	The new humidity value
	 * 			|double newHumidity = 0;
	 * 			|Iterator<Square> roomIterator = room.iterator();
	 * 			|while(roomIterator.hasNext()){
	 * 			|	newHumidity += roomIterator.next().getHumidity();
	 * 			|}
	 * 			|newHumidity /= room.size();
	 * 			|int newHumidityRounder =(int)(newHumidity * 100);
	 * 			|newHumidity = newHumidityRounder / 100;
	 * 			|return == newHumidity;
	 */
	@Model
	private static double calculateNewRoomHumidity(HashSet<? extends Square> room2) {
		double newHumidity = 0;
		Iterator<? extends Square> roomIterator = room2.iterator();
		while(roomIterator.hasNext()){
			newHumidity += roomIterator.next().getHumidity();
		}
		newHumidity /= room2.size();
		int newHumidityRounder =(int)(newHumidity * 100);
		newHumidity = newHumidityRounder / 100;
		return newHumidity;
	}
	
	/**
	 * Calculates a new temperature that is the weighted average of all
	 * temperatures inside a room
	 * @param room2
	 * 			The room for which to calculate the new temperature
	 * @param averageHumidity
	 * 			The average humidity inside this particular room
	 * @return	The average weighted temperature for the room - calculated
	 * 			system-dependently
	 */
	@Model
	private static Temperature calculateNewRoomTemperature(HashSet<Square> room2, double averageHumidity) {
		BigDecimal newTemperatureNumeral = BigDecimal.ZERO;
		Iterator<Square> roomIterator = room2.iterator();
		while(roomIterator.hasNext()){
			Square square = roomIterator.next();
			BigDecimal temperatureNumeral = square.getTemperature().getNumeral();
			double humidityFactor = square.getHumidity() / averageHumidity;
			temperatureNumeral = temperatureNumeral.multiply(BigDecimal.valueOf(humidityFactor));
			newTemperatureNumeral = newTemperatureNumeral.add(temperatureNumeral);
		}
		newTemperatureNumeral = newTemperatureNumeral.multiply(BigDecimal.ONE.subtract(BigDecimal.valueOf(Square.getTemperatureMergingConstant())));
		newTemperatureNumeral = newTemperatureNumeral.divide(BigDecimal.valueOf(room2.size()),RoundingMode.HALF_EVEN);
		newTemperatureNumeral = newTemperatureNumeral.divide(BigDecimal.ONE.subtract(BigDecimal.valueOf(Square.getTemperatureMergingConstant())),RoundingMode.HALF_EVEN);
		return new Temperature(newTemperatureNumeral);
	}

	/**
	 * A method to enforce the room this square is inside is again consistent with
	 * its surrounding and containing borders.
	 * Uses a 3D FloodFill variant for detection of all other squares in the room.
	 * Runtime: O(n) or linear time.
	 * 
	 * @param square
	 * 			The square whose room will be rechecked
	 * @post 	All squares in the 'room' enclosing the square object will really
	 * 			be connected to this square without obstacles
	 * 			|Dungeon dungeon = New.Dungeon.getDungeonContainingSquare(square)
	 * 			|HashSet<Square> room = dungeon.getEnclosingRoom(square);
	 * 			|Iterator<Square> roomIterator = room.iterator();
	 * 			|while(roomIterator.hasNext()){
	 * 			|	Square otherSquare = roomIterator.next();
	 * 			|	for(Direction direction : Direction.values()){
	 * 			|		if(!isSquareBorder(square,direction)
	 * 			|			&& dungeon.contains(getNextSquareInDirection(otherSquare,direction)))
	 * 			|		then room.contains(getNextSquareInDirection(otherSquare,direction)) == true;
	 * 			|	}
	 * 			|}	
	 * @effect	The squares' data in this room will be recalculated
	 * 			|refreshEnclosingRoomData(square);
	 * @throws IllegalArgumentException
	 * 			The square must be initialized
	 * 			|(square == null) || (square.isTerminated())
	 * @throws IllegalArgumentException
	 * 			The square must be inside any dungeon
	 * 			|!Dungeon.isSquareInsideAnyDungeon(square)
	 */
	public static void enforceRoomBorders(Square square) throws IllegalArgumentException{
		if(square instanceof Rock)
			throw new IllegalArgumentException("Cant expand the room around a rock");
		dungeon = Dungeon.getDungeonContainingSquare(square); 
		room = new HashSet<Square>();
		HashSet<Square> hashSetContainingOnlySquare = new HashSet<Square>();
		hashSetContainingOnlySquare.add(square);
		expandRoomFromSquare((TeleportationSquare) square);
		for(Square squareInRoom : room)
			Dungeon.getDungeonContainingSquare(squareInRoom).setRoom(squareInRoom, room);
		loopDetected = false;
		refreshEnclosingRoomData(square);
	}

	/**
	 * Maps the room around a certain square from that square outwards using
	 * the floodfill algorithm.
	 * 
	 * @param square
	 * 			The square whose room will be mapped
	 */
	private static void expandRoomFromSquare(TeleportationSquare square){
		HashMap<Direction, HashMap<Direction,HashSet<Square>>> squaresFound = new HashMap<Direction, HashMap<Direction,HashSet<Square>>>();
		HashMap<Direction,HashSet<Square>> startingMap = new HashMap<Direction, HashSet<Square>>();
		HashSet<Square> startingSquare = new HashSet<Square>();
		startingSquare.add(square);
		room.add(square);
		for(Direction tempDir : Direction.values())
			startingMap.put(tempDir, startingSquare);
		for(Direction tempDir : Direction.values())
			squaresFound.put(tempDir, startingMap);
		Direction extendingDirection = Direction.NORTH;//The direction in which we extend our flood
		boolean keepGoing = true;
		while(keepGoing){
			keepGoing = false;
			outerLoop:
			for(Direction tempDir : Direction.values())
				for(Direction innerTempDir : Direction.values()){
					if(squaresFound.get(tempDir).get(innerTempDir).size() != 0){
						keepGoing = true;
						break outerLoop;
					}
				}
			HashMap<Direction,HashSet<Square>> squaresFoundInDirection = squaresFound.get(extendingDirection);
			HashSet<Square> foundSquares = new HashSet<Square>();
			for(Direction checkingDirection : Direction.values()){
				HashSet<Square> squaresToCheckInDirection = squaresFoundInDirection.get(checkingDirection);
				if((squaresToCheckInDirection != null) && (squaresToCheckInDirection.size() != 0)){
					Iterator<Square> squaresToCheckIterator = squaresToCheckInDirection.iterator();
					while(squaresToCheckIterator.hasNext()){
						Square nextSquare = (TeleportationSquare) squaresToCheckIterator.next();
						while(!isSquareBorder(nextSquare, extendingDirection)){
							nextSquare = getNextSquareInDirection(nextSquare, extendingDirection);
							if(!room.contains(nextSquare))
							foundSquares.add(nextSquare);
							room.add((TeleportationSquare) nextSquare);
						}
					}
				}
			}
			for(Direction direction : Direction.values())
				squaresFound.get(direction).put(extendingDirection, new HashSet<Square>(foundSquares));
//			squaresFound.get(extendingDirection).remove(extendingDirection);
			extendingDirection = directionOrder.get(extendingDirection);
		}
	}
	/**
	 * Checks if a square is at the border of a room in a specific direction
	 * 
	 * @param square
	 * 			The square to check
	 * @param direction
	 * 			The direction in which to check
	 * @return Whether or not the square has an obstacle in a specific direction
	 * 			|if(square.getBorder(direction) == null)
	 * 			|	result == false;
	 * 			|else result == square.getBorder(direction).isObstacle()
	 * @throws IllegalArgumentException
	 * 			All parameters must be initialized
	 * 			|(square == null) || (square.isTerminated())|| (direction == null)
	 */
	@Model
	private static boolean isSquareBorder(Square square, Direction direction) throws IllegalArgumentException{
		if((square == null) || (square.isTerminated()) || (direction == null))
			throw new IllegalArgumentException("Please initialize all parameters");
		if((square.getBorder(direction) == null) || !square.getBorder(direction).isObstacle()){
			if(getNextSquareInDirection(square, direction) == null)
				return true;
			if((getNextSquareInDirection(square, direction).getBorder(direction.getOppositeDirection()) == null))
				return false;
			if(getNextSquareInDirection(square, direction).getBorder(direction.getOppositeDirection()).isObstacle())
				return true;
			return false;
		}
		return square.getBorder(direction).isObstacle();
	}

	/**
	 * Gets the next square in a given direction
	 * 
	 * @param square
	 * 			The current square
	 * @param direction
	 * 			The direction in which to check
	 * @return The next square in the given direction
	 * 			|if(squareBordersNegativeCoordinatesInDirection(square, direction))
	 *			|	result == null;
	 * 			|Coordinate nextSquareCoordinate = dungeon.getCoordinateOfSquare(square)
	 * 			|	.getNextCoordinateInDirection(direction);
	 * 			|if(!dungeon.contains(nextSquareCoordinate))
	 * 			|	result == null
	 * 			|else result == dungeon.getSquareAtCoordinate(nextSquareCoordinate)
	 * @throws IllegalArgumentException
	 * 			The parameters must be initialized
	 * 			|(square == null) || (square.isTerminated()) || (direction == null)
	 */
	public static Square getNextSquareInDirection(Square square, Direction direction) throws IllegalArgumentException{
		if((square == null) || (square.isTerminated()) || (direction == null))
			throw new IllegalArgumentException("Please initialize all parameters");
		Dungeon<Square> dungeon = Dungeon.getDungeonContainingSquare(square);
		if(squareBordersBorderCoordinatesInDirection(square, direction)){
			while(dungeon.hasParentDungeon()){
				@SuppressWarnings("unchecked")
				CompositeDungeon<Square> parentDungeon = (CompositeDungeon<Square>) dungeon.getParentDungeon();
				Coordinate parentCoordinate = parentDungeon.calculateCoordinateInThisDungeon(dungeon
						.getCoordinateOfSquare(square), dungeon);
				if(coordinateBordersNegativeCoordinatesInDirection(parentCoordinate, direction))
					dungeon = parentDungeon;
				else if(parentDungeon.hasChildDungeonContainingCoordinate(parentCoordinate.getNextCoordinateInDirection(direction))){
					if(parentDungeon.contains(parentCoordinate.getNextCoordinateInDirection(direction)))
						return parentDungeon.getSquareAtCoordinate(parentCoordinate.getNextCoordinateInDirection(direction));
					else
						dungeon = parentDungeon;
				}
				else
					dungeon = parentDungeon;
			}
			return null;
		}
		Coordinate nextSquareCoordinate = dungeon.getCoordinateOfSquare(square).getNextCoordinateInDirection(direction);
		if(!dungeon.contains(nextSquareCoordinate))
			return null;
		return dungeon.getSquareAtCoordinate(nextSquareCoordinate);
	}

	/**
	 * Checks whether or not the square borders one of the outer sides of the dungeon
	 * it's in. This is for checking whether rooms will be calculated across dungeon
	 * borders.
	 * 
	 * @param square
	 * 			The square for whom to check
	 * @param direction
	 * 			The direction in which to check
	 * @return Whether or not the square borders the dungeon
	 * 			|if(!Dungeon.isSquareInsideAnyDungeon(square))
	 * 			|	then result == false
	 * 			|switch(direction){
	 *			|	case WEST:
	 *			|		result == (dungeon.getCoordinateOfSquare(square).getxCoordinate() == 0)
	 *			|	case SOUTH:
	 *			|		result == (dungeon.getCoordinateOfSquare(square).getyCoordinate() == 0)
	 *			|	case FLOOR:
	 *			|		result == (dungeon.getCoordinateOfSquare(square).getzCoordinate() == 0)
	 *			|	case EAST:
	 *			|		result == (dungeon.getCoordinateOfSquare(square).getxCoordinate() == dungeon.getXSize() -1)
	 *			|	case NORTH:
	 *			|		result == (dungeon.getCoordinateOfSquare(square).getyCoordinate() == dungeon.getYSize() -1)
	 *			|	case CEILING:
	 *			|		result == (dungeon.getCoordinateOfSquare(square).getzCoordinate() == dungeon.getZSize() -1)
	 *			|}
	 *			|result == false
	 */
	@Model
	private static boolean squareBordersBorderCoordinatesInDirection(Square square, Direction direction){
		if(!Dungeon.isSquareInsideAnyDungeon(square))
			return false;
		dungeon = Dungeon.getDungeonContainingSquare(square);
		switch(direction){
		case WEST:
			return (dungeon.getCoordinateOfSquare(square).getxCoordinate() == 0);
		case SOUTH:
			return (dungeon.getCoordinateOfSquare(square).getyCoordinate() == 0);
		case FLOOR:
			return (dungeon.getCoordinateOfSquare(square).getzCoordinate() == 0);
		case EAST:
			return (dungeon.getCoordinateOfSquare(square).getxCoordinate() == dungeon.getXSize() -1);
		case NORTH:
			return (dungeon.getCoordinateOfSquare(square).getyCoordinate() == dungeon.getYSize() -1);
		case CEILING:
			return (dungeon.getCoordinateOfSquare(square).getzCoordinate() == dungeon.getZSize() -1);
		}
		return false;
	}
	
	/**
	 * Checks whether or not the square would borders has zero x, y or z coordinate
	 * depending on the direction. This would mean that the next square in that direction
	 * has a negative (and thus disallowed) coordinate.
	 * 
	 * @param square
	 * 			The square for whom to check
	 * @param direction
	 * 			The direction in which to check
	 * @return Whether or not the square borders the dungeon
	 * 			|switch(direction){
	 *			|	case WEST:
	 *			|		result == (dungeon.getCoordinateOfSquare(square).getxCoordinate() == 0);
	 *			|	case SOUTH:
	 *			|		result == (dungeon.getCoordinateOfSquare(square).getyCoordinate() == 0);
	 *			|	case FLOOR:
	 *			|		result == (dungeon.getCoordinateOfSquare(square).getzCoordinate() == 0);
	 *			|}
	 *			|result == false;
	 */
	@Model
	private static boolean coordinateBordersNegativeCoordinatesInDirection(Coordinate coordinate, Direction direction){
		switch(direction){
		case WEST:
			return (coordinate.getxCoordinate() == 0);
		case SOUTH:
			return (coordinate.getyCoordinate() == 0);
		case FLOOR:
			return (coordinate.getzCoordinate() == 0);
		}
		return false;
	}

	/**
	 * A method for ensuring 2-way relationships between borders, 
	 * squares and their neighbors. Thus, if a square has a neighbor
	 * in the direction in which a border is added, then this neighbor
	 * will have the same border set in the opposite direction.
	 * The border itself will also have the initial square set in the
	 * opposite direction, and the neighboring square in the initial
	 * direction.
	 * If the square has no neighboring square in the initial direction
	 * the parameter won't be set either for border.
	 * @param square
	 * 			The initial square whose border will be set
	 * @param border
	 * 			The border that will be set
	 * @param direction
	 * 			The direction in which to set the border
	 * @effect Enforces the room borders of both this square and its neighbor
	 * 			|enforceRoomBorders(square);
	 * 			|enforceRoomBorders(RoomHandler.getNextSquareInDirection(square, direction));
	 * @throws IllegalArgumentException
	 * 			The border and square(s) must be able to have each other
	 * 			as border or square.
	 * 			|!square.canHaveAsBorderInDirection(border, direction)
	 * 			|!border.canHaveAsSquareInDirection(square, direction.getOppositeDirection())
	 * 			|Square otherSquare = RoomHandler.getNextSquareInDirection(square, direction);
	 * 			|if((otherSquare != null) && !otherSquare.isTerminated)
	 * 			|	then{
	 * 			|		!border.canHaveAsSquareInDirection(otherSquare, direction)
	 * 			|		!otherSquare.canHaveAsBorderInDirection(border, direction.getOppositeDirection())
	 */
	public static void setBorder(Square square, Border border, Direction direction) throws IllegalArgumentException{
		forceSetBorder(square, border, direction);
		if(Dungeon.isSquareInsideAnyDungeon(square)){
			dungeon = Dungeon.getDungeonContainingSquare(square);
			Square otherSquare = getNextSquareInDirection(square, direction);
			if((otherSquare != null) && !otherSquare.isTerminated()){
				forceSetBorder(otherSquare, border, direction.getOppositeDirection());
				if(!(square instanceof Rock))
					enforceRoomBorders((TeleportationSquare) square);
				if(!(otherSquare instanceof Rock))
					enforceRoomBorders((TeleportationSquare) otherSquare);
			}
		}
	}
	
	/**
	 * Forces setting of the borders of a square, and the inverse relationship
	 * 
	 * @param square
	 * 			The square whose border to set
	 * @param border
	 * 			The border to set
	 * @param direction
	 * 			The direction in which the border needs to be set
	 * @effect 	Sets the squares border
	 * 			|square.setBorder(border,direction)
	 * @effect	Sets the borders square
	 * 			|border.setSquareInDirection(square,direction.getOppositeDirection())
	 * @throws IllegalArgumentException
	 * 			The square must be able the have the border in that direction
	 * 			|!square.canHaveAsBorderInDirection(border, direction)
	 * @throws IllegalArgumentException
	 * 			The border must be able to have the square in that direction
	 * 			|!border.canHaveAsSquareInDirection(square, direction.getOppositeDirection())
	 */
	private static void forceSetBorder(Square square, Border border, Direction direction) throws IllegalArgumentException{
		if(!square.canHaveAsBorderInDirection(border, direction))
			throw new IllegalArgumentException("The square can't have the border in that direction");
		if( (border!=null)&&(!border.canHaveAsSquareInDirection(square, direction.getOppositeDirection())) )
			throw new IllegalArgumentException("The border can't have the square in that direction");
		square.setBorder(border,direction);
		if(border!=null)
			border.setSquareInDirection(square,direction.getOppositeDirection());
	}
}
